/**
 * \file
 */
#include "precompiled.hpp"
#include "kfs_cache.hpp"
#include "kfs_impl.hpp"
#include "kfs_file_descriptor.hpp"
#include "kfs_time.hpp"
#include "Exception.hpp"
#include <boost/bind.hpp>
#include <algorithm>
#include <errno.h>            // EINVAL et al
#include <fcntl.h>            // O_CREAT et al
#include <map>
#include <memory>
#include <sys/stat.h>         // S_ISDIR et al
#include <unistd.h>           // mode_t, uid_t, gid_t

using boost::shared_ptr;
using std::auto_ptr;  using std::map;  using std::string;

extern kfs* HACK_fs;

//-----------------------------------------------------------------------------

/**
 * Return true iff the cache entry has the specified inode number.
 */
static bool inodeCacheKeyPred(block_index_t const& desiredInodeNum,
    InodeCache::value_type const& elt)
{
    return elt.second->blockNum == desiredInodeNum;
}

//-----------------------------------------------------------------------------


// TODO: Consider deprecating this in favour of cache_get.  They
// basically do the same thing except cache_add requires the caller
// to populate the element whereas cache_get(blockid) reads the inode
// from disk and creates a cache element.
CacheRef*
kfs::Impl::cache_add_from_memory(InodeCache::value_type const& elt)
{
    std::pair<InodeCache::iterator, bool> result =
        inodeCache.insert( elt );
    if(result.second == false)
    {
        throw Error(EEXIST, "%s: cache entry already exists", __FUNCTION__);
    }
    // Return an iterator to the new cache entry.
    return new CacheRef(this, result.first);
}


CacheRef*
kfs::Impl::cache_get(string const& path)
{
    InodeCache::iterator i_path = inodeCache.find(path);
    if(i_path == inodeCache.end())
    {
        // Look up the inode. Throw if it's not there.
        boost::scoped_ptr<InodeRef> ref1(traverse(path, HACK_fs));

        // Create a cache element for this inode.
        shared_ptr<CacheInode> elt(new CacheInode);
        elt->blockNum = ref1->diskBlockNum;
        elt->inode = ref1->inode;

        // Insert it into the cache.
        std::pair<InodeCache::iterator, bool> result =
            inodeCache.insert( make_pair( path, elt ) );
        i_path = result.first;
    }
    return new CacheRef(this, i_path);
}


CacheRef*
kfs::Impl::cache_get_from_disk(block_index_t const& inodeID,
    string const& path)
{
    // TODO: Sublinear look-ups by inode number!
    InodeCache::iterator i_path = std::find_if(inodeCache.begin(),
        inodeCache.end(),
        boost::bind( &inodeCacheKeyPred, inodeID, _1 ) );
    if(i_path == inodeCache.end())
    {
        // Read the inode from disk. Extract into a new cache element.
        shared_ptr<CacheInode> elt(new CacheInode);
        elt->blockNum = inodeID;
        boost::scoped_array<uint8_t> block(createBlockBuffer());
        kfs_disk_read(*this, inodeID, block.get());
        deserialize(elt->inode, block.get());

        // Insert it into the cache.
        std::pair<InodeCache::iterator, bool> result =
            inodeCache.insert( make_pair( path, elt ) );
        i_path = result.first;
    }
    return new CacheRef(this, i_path);
}

void
kfs::Impl::cache_destroy()
{
    for(InodeCache::iterator i = inodeCache.begin(); i != inodeCache.end();
        ++i)
    {
        cache_flush(i->second.get());
    }
}


void
kfs::Impl::cache_flush(CacheInode* inodeRef)//InodeCache::value_type& elt)
{
    if(inodeRef->dirty)
    {
        boost::scoped_array<uint8_t> block(createBlockBuffer());
        serialize(inodeRef->inode, block.get());
        kfs_disk_write(*this, inodeRef->blockNum, block.get());
        inodeRef->dirty = false;
    }
}


void
kfs::Impl::cache_remove(InodeCache::iterator elt)
{
    if(elt->second->referenceCount == 0)
    {
        if(elt->second->dirty)
        {
            cache_flush(elt->second.get());
        }
        inodeCache.erase(elt);
    }
}

//-----------------------------------------------------------------------------


CacheRef::~CacheRef()
{
    if(elt->second->referenceCount == 0)
    {
        fprintf(stderr,"%s: inode %u: reference count is already zero\n",
            __FUNCTION__, elt->second->blockNum);
    }
    else if(--elt->second->referenceCount == 0)
    {
        impl->cache_remove(elt);
    }
}


void CacheRef::write()
{
    // Set inode change time-stamp (ctime).
    elt->second->inode.ctime = KfsTime::timeNow();
    elt->second->dirty = true;
    impl->cache_flush(elt->second.get());
}

//--------------------------------------------


CacheInode::CacheInode()
:
    blockNum(0),
    dirty(false),
    inode(),
    referenceCount(0)
{
}


CacheInode::~CacheInode()
{
}
